package com.dksidana.java.jdbc.bean;

import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class BeanHandler {
	/**
	 * Set a bean's primitive properties to these defaults when SQL NULL 
	 * is returned.  These are the same as the defaults that ResultSet get* 
	 * methods return in the event of a NULL column.
	 */
	private static final Map<Class<?>,Object> primitiveDefaults = new HashMap<Class<?>, Object>();

	static {
		primitiveDefaults.put(Integer.TYPE, new Integer(0));
		primitiveDefaults.put(Short.TYPE, new Short((short) 0));
		primitiveDefaults.put(Byte.TYPE, new Byte((byte) 0));
		primitiveDefaults.put(Float.TYPE, new Float(0));
		primitiveDefaults.put(Double.TYPE, new Double(0));
		primitiveDefaults.put(Long.TYPE, new Long(0));
		primitiveDefaults.put(Boolean.TYPE, Boolean.FALSE);
		primitiveDefaults.put(Character.TYPE, new Character('\u0000'));
	}


	public static <T> T toBean(ResultSet rs, Class<T> type) throws SQLException {
		PropertyDescriptor[] propDesc = getPropertyDescriptors(rs,type);		
		return createBean(rs, type, propDesc);
	}
	public static <T> List<T> toBeanList(ResultSet rs, Class<T> type) throws SQLException {
		List<T> results = new ArrayList<T>();

		if (!rs.next()) {
			return results;
		}
		PropertyDescriptor[] propDesc = getPropertyDescriptors(rs, type);
		do {
			results.add(createBean(rs, type, propDesc));
		} while (rs.next());
		return results;
	}

	protected static <T> PropertyDescriptor[]  getPropertyDescriptors(ResultSet resultSet,Class<T> type) throws SQLException {
		return getPropertyDescriptors(resultSet.getMetaData(), type);
	}
	/**
	 * Returns a PropertyDescriptor[] for the given Class.
	 * @param rsmd ResultSetMetaData
	 * @param type The Class to retrieve PropertyDescriptors for.
	 * @return A PropertyDescriptor[] describing the Class, in order of ResultSet's Columns.
	 * @throws SQLException if introspection failed.
	 */
	protected static <T> PropertyDescriptor[]  getPropertyDescriptors(ResultSetMetaData rsmd,Class<T> type) throws SQLException {
		try
		{
			PropertyDescriptor[] props = Introspector.getBeanInfo(type).getPropertyDescriptors();
			int cols = rsmd.getColumnCount();
			PropertyDescriptor propertyDescriptor[] = new PropertyDescriptor[cols + 1];
			for (int col = 1; col <= cols; col++) {
				propertyDescriptor[col]=null;
				String columnName = rsmd.getColumnName(col);
				for (int i = 0; i < props.length; i++) {
					if (columnName.equalsIgnoreCase(props[i].getName())) {
						propertyDescriptor[col]=propertyDescriptor[i];
						break;
					}
				}
			}
			return propertyDescriptor;
		}
		catch (IntrospectionException e) {
			throw new SQLException("Bean introspection failed: " + e.getMessage());
		}
	}
	private static <T> T createBean(ResultSet rs,Class<T> type,PropertyDescriptor[] props) throws SQLException
	{
		T bean=createInstance(type);
		setBeanProperties(rs, bean, props);
		return bean;
	}
	private static <T> T createInstance(Class<T> type) throws SQLException
	{
		T bean=null;
		try {
			bean=type.newInstance();
		} catch (InstantiationException e) {
			throw new SQLException("Cannot create " + type.getName() + ": " + e.getMessage());

		} catch (IllegalAccessException e) {
			throw new SQLException("Cannot create " + type.getName() + ": " + e.getMessage());
		}
		return bean;
	}
	private static <T> void setBeanProperties(ResultSet rs, T bean,PropertyDescriptor[] props)
	throws SQLException {
		
		for (int i = 1; i < props.length; i++) {
			//complete code can be replaced by following line
			//props[i].getWriteMethod().invoke(bean, rs.getObject(i));
			setBeanProperty(rs, i, bean, props[i]);
		}
	}

	private static <T> void setBeanProperty(ResultSet rs,int i,T bean,PropertyDescriptor prop) throws SQLException
	{
		if (prop==null)  
			return;

		Class<?> propType = prop.getPropertyType();
		if(propType==null)
			return;

		Method setter = prop.getWriteMethod();
		if (setter == null) 
			return;

		Object value = processColumn(rs, i, propType);

		if (propType != null && value == null && propType.isPrimitive()) {
			value = primitiveDefaults.get(propType);
		}
		Class<?>[] params = setter.getParameterTypes();
		value=doSomeSmartness(value,params[0]);
		try
		{
			// Don't call setter if the value object isn't the right type 
			if (isCompatibleType(value, params[0])) {
				setter.invoke(bean, new Object[] { value });
			} else {
				throw new SQLException("Cannot set " + prop.getName() + ": incompatible types.");
			}
		}
		catch (IllegalAccessException e) {
			throw new SQLException("Cannot set " + prop.getName() + ": " + e.getMessage());
		} catch (InvocationTargetException e) {
			throw new SQLException("Cannot set " + prop.getName() + ": " + e.getMessage());
		}catch (IllegalArgumentException e) {
			throw new SQLException("Cannot set " + prop.getName() + ": " + e.getMessage());
		}
	}
	private static <T> Object doSomeSmartness(Object value,Class<?> setterParameterClass) throws SQLException
	{
		// convert types for some popular ones
		if (value != null) {
			if (value instanceof java.util.Date) {
				if (setterParameterClass.getName().equals("java.sql.Date")) {
					value = new java.sql.Date(((java.util.Date) value).getTime());
				} else if (setterParameterClass.getName().equals("java.sql.Time")) {
					value = new java.sql.Time(((java.util.Date) value).getTime());
				} else if (setterParameterClass.getName().equals("java.sql.Timestamp")) {
					value = new java.sql.Timestamp(((java.util.Date) value).getTime());
				}
			}
		}
		return value;
	}
	/**
	 * ResultSet.getObject() returns an Integer object for an INT column.  The
	 * setter method for the property might take an Integer or a primitive int.
	 * This method returns true if the value can be successfully passed into
	 * the setter method.  Remember, Method.invoke() handles the unwrapping
	 * of Integer into an int.
	 * 
	 * @param value The value to be passed into the setter method.
	 * @param type The setter's parameter type.
	 * @return boolean True if the value is compatible.
	 */
	private static boolean isCompatibleType(Object value, Class<?> type) {
		// Do object check first, then primitives
		if (value == null || type.isInstance(value)) {
			return true;

		} else if (
				type.equals(Integer.TYPE) && Integer.class.isInstance(value)) {
			return true;

		} else if (type.equals(Long.TYPE) && Long.class.isInstance(value)) {
			return true;

		} else if (
				type.equals(Double.TYPE) && Double.class.isInstance(value)) {
			return true;

		} else if (type.equals(Float.TYPE) && Float.class.isInstance(value)) {
			return true;

		} else if (type.equals(Short.TYPE) && Short.class.isInstance(value)) {
			return true;

		} else if (type.equals(Byte.TYPE) && Byte.class.isInstance(value)) {
			return true;

		} else if (
				type.equals(Character.TYPE) && Character.class.isInstance(value)) {
			return true;

		} else if (
				type.equals(Boolean.TYPE) && Boolean.class.isInstance(value)) {
			return true;

		} else {
			return false;
		}

	}
	/**
	 * Convert a <code>ResultSet</code> column into an object.  Simple 
	 * implementations could just call <code>rs.getObject(index)</code> while
	 * more complex implementations could perform type manipulation to match 
	 * the column's type to the bean property type.
	 * 
	 * <p>
	 * This implementation calls the appropriate <code>ResultSet</code> getter 
	 * method for the given property type to perform the type conversion.  If 
	 * the property type doesn't match one of the supported 
	 * <code>ResultSet</code> types, <code>getObject</code> is called.
	 * </p>
	 * 
	 * @param rs The <code>ResultSet</code> currently being processed.  It is
	 * positioned on a valid row before being passed into this method.
	 * 
	 * @param index The current column index being processed.
	 * 
	 * @param propType The bean property type that this column needs to be
	 * converted into.
	 * 
	 * @throws SQLException if a database access error occurs
	 * 
	 * @return The object from the <code>ResultSet</code> at the given column
	 * index after optional type processing or <code>null</code> if the column
	 * value was SQL NULL.
	 */
	protected static Object processColumn(ResultSet rs, int index, Class<?> propType)
	throws SQLException {

		if ( !propType.isPrimitive() && rs.getObject(index) == null ) {
			return null;
		}

		if (propType.equals(String.class)) {
			return rs.getString(index);

		} else if (propType.equals(Integer.TYPE) || propType.equals(Integer.class)) {
			return new Integer(rs.getInt(index));

		} else if (propType.equals(Boolean.TYPE) || propType.equals(Boolean.class)) {
			return new Boolean(rs.getBoolean(index));

		} else if (propType.equals(Long.TYPE) || propType.equals(Long.class)) {
			return new Long(rs.getLong(index));

		} else if (propType.equals(Double.TYPE) || propType.equals(Double.class)) {
			return new Double(rs.getDouble(index));

		} else if (propType.equals(Float.TYPE) || propType.equals(Float.class)) {
			return new Float(rs.getFloat(index));

		} else if (propType.equals(Short.TYPE) || propType.equals(Short.class)) {
			return new Short(rs.getShort(index));

		} else if (propType.equals(Byte.TYPE) || propType.equals(Byte.class)) {
			return new Byte(rs.getByte(index));

		} else if (propType.equals(Timestamp.class)) {
			return rs.getTimestamp(index);
		} else {
			return rs.getObject(index);
		}

	}
}
